package top.jacktgq.candychat.netty;

import java.util.ArrayList;
import java.util.List;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import top.jacktgq.candychat.netty.pojo.dto.ChatMsgDTO;
import top.jacktgq.candychat.netty.utils.SpringUtil;
import top.jacktgq.candychat.pojo.ChatMsg;
import top.jacktgq.candychat.netty.pojo.enums.MsgActionEnum;
import top.jacktgq.candychat.netty.pojo.dto.DataContentDTO;
import top.jacktgq.candychat.service.UserinfoService;
import top.jacktgq.candychat.service.impl.UserinfoServiceImpl;
import top.jacktgq.candychat.utils.JsonUtils;

/**
 * @Title: ChatHandler.java 
 * @Package top.jacktgq.imooc.groupchat 
 * @Description: 处理WebSocket消息的Handler
 *                  TextWebSocketFrame：在Netty中，是用于为WebSocket专门处理文本的对象，frame是消息的载体
 * @author CandyWall   
 * @date 2021年2月4日 下午10:19:57 
 * @version V1.0
 */
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    // 用于记录和管理所有在线的客户端的Channel
    public static final ChannelGroup userChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    // 保存所有用户ID和ChannelId之间的对应关系
    public static final UserIdChannelMap userIdChannelMap = new UserIdChannelMap();

    /**
     * 服务器主动推送消息给客户端
     *     使用场景：
     *         1.接收方通过好友请求后，服务器发送消息给发送方，刷新通讯录，重新拉取好友列表
     * @param acceptUserId
     * @param dataContentDTO
     */
    public static void sendMsg(String acceptUserId, DataContentDTO dataContentDTO) {
        Channel acceptChannel = userIdChannelMap.get(acceptUserId);
        // 查询用户是否在线
        if (acceptChannel != null) {
            try {
                acceptChannel.writeAndFlush(new TextWebSocketFrame(JsonUtils.obj2json(dataContentDTO)));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 获取用户的Channel
        Channel sendUserChannel = ctx.channel();
        
        // 1.获取客户端传输过来的消息
        String content = msg.text();
        System.out.println("接收到的数据：" + content);
        // 2.判断消息类型，根据不同的类型，处理不同的业务
        DataContentDTO dataContentDTO = JsonUtils.json2pojo(content, DataContentDTO.class);
        // 2.1 当Websocket第一次open的时候初始化Channel，把用户的Channel和userId关联起来
        if(dataContentDTO.getAction() == MsgActionEnum.CONNECT.type) {
            String getSendUserId = dataContentDTO.getChatMsgDTO().getSendUserId();
            userIdChannelMap.put(getSendUserId, sendUserChannel);
            System.out.println("从userIdChannelMap中移除了userId为 " + getSendUserId + " 的记录");
        }
        // 2.2 聊天类型的消息，把聊天记录保存到数据库，同时标记消息的签收状态[默认未签收]
        else if(dataContentDTO.getAction() == MsgActionEnum.CHAT.type) {
            ChatMsgDTO chatMsgDTO = dataContentDTO.getChatMsgDTO();
            // 保存消息到数据库，并且标记为未签收
            ChatMsg chatMsg = new ChatMsg();
            BeanUtils.copyProperties(chatMsgDTO, chatMsg);
            UserinfoService userinfoService = (UserinfoService) SpringUtil.getBean(UserinfoServiceImpl.class);
            userinfoService.saveChatMsg(chatMsg);
            // 设置消息ID给DTO对象，以便后续签收消息使用
            chatMsgDTO.setMsgId(chatMsg.getId());
            // 从全局用户Channel关系表中获取消息接收方的Channel
            Channel acceptUserChannel = userIdChannelMap.get(chatMsgDTO.getAcceptUserId());
            // 如果不为空，表示用户在线
            if (acceptUserChannel != null) {
                // 发送消息
                acceptUserChannel.writeAndFlush(new TextWebSocketFrame(JsonUtils.obj2json(dataContentDTO)));
            }
            // 用户不在线
            else {
                System.out.println("用户不在线...");
            }
        }
        // 2.3 签收类型的消息，这是针对具体的消息进行签收，修改数据库中对应消息的签收状态
        // 这里的签收不是指消息是否被接收方用户已读和未读的状态，而是指消息是否发送到接收方的手机上
        else if(dataContentDTO.getAction() == MsgActionEnum.SIGNED.type) {
            UserinfoService userinfoService = (UserinfoService) SpringUtil.getBean(UserinfoServiceImpl.class);
            // 扩展字段只有在签收类型的消息中才会被赋值，代表需要取签收的消息
            // 多条待签收的消息的ID以逗号隔开存储在extend中（注意：逗号前后没有空格）
            String[] msgIds = dataContentDTO.getExtend().split(",");
            List<String> msgIdList = new ArrayList<>();
            // 将非空的id存到list集合中
            for (String msgId : msgIds) {
                if (StringUtils.isNotBlank(msgId)) {
                    msgIdList.add(msgId);
                }
            }
            if (!msgIdList.isEmpty()) {
                // 批量签收消息
                userinfoService.updateMsgSignedBatch(msgIdList);
            }
        }
        // 2.4 心跳类型的消息
        else if(dataContentDTO.getAction() == MsgActionEnum.KEEPALIVE.type) {
            System.out.println("收到来自Channel为[" + sendUserChannel + "]的心跳包...");
        }
        /*TextWebSocketFrame responseMsg = new TextWebSocketFrame("服务器时间：" + LogUtils.getCurrentTime() + " " + msg.text());
        userChannelGroup.writeAndFlush(responseMsg);*/
    }
    
    //当web客户端连接后，就会触发方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("有新的WebSocket客户端接入：LongText：" + ctx.channel().id().asLongText() + "，ShortText：" + ctx.channel().id().asShortText());
        // 获取客户端的Channel，并且放到ChannelGroup中进行管理
        userChannelGroup.add(ctx.channel());
    }

    //当web客户端断开连接后，就会触发该方法
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("WebSocket客户端断开：长ID：" + ctx.channel().id().asLongText() + "短ID：" + ctx.channel().id().asShortText());
        // channelGroup.remove(ctx.channel());
        // 这里不需要自己去把当前的channel从channelGroup中移除，netty内部已经实现
        // 当触发handlerRemoved，ChannelGroup内部会自动移除对应客户端的channel
        // 相应地，userIdChannelIdMap中也删除一条记录
        userIdChannelMap.removeByChannel(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        // 发生异常之后关闭连接，随后从ChannelGroup中移除
        ctx.channel().close();
        userChannelGroup.remove(ctx);
        userIdChannelMap.removeByChannel(ctx.channel());
    }
}
